package com.project.search;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.search.model.DocInfo;
import com.project.search.model.Weight;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 通过这个类在内存中构造出索引结构
public class Index {
    // 引入 Jackson 库中的一个核心对象
    private ObjectMapper objectMapper = new ObjectMapper();

    // 保存索引结构的磁盘文件
    private static final String SAVE_PATH = "/root/save_index_structure/";

    // 创建两个锁对象
    private Object locker1 = new Object();
    private Object locker2 = new Object();

    // 使用数组下标来表示 docId, 该变量即为正排索引结构
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    // 使用 哈希表 来表示倒排索引
    // key 表示 词
    // value 表示一组 和该词关联的文章(Weight 表示文章以及词与文章的相关性权重)
    private HashMap<String, ArrayList<Weight>> invertedIndex = new HashMap<>();

    // 这个类要提供的方法:
    // 1. 给定一个 docId, 在正排索引中, 查询文档的详细信息.
    // 既然是查询文档详细信息, 就需要一个文档实体类来表示
    public DocInfo getDocInfo(int docId) {
        return forwardIndex.get(docId);
    }

    // 2. 给定一个 词, 在倒排索引中, 查找哪些文档和这个词有关系(即该方法是使用倒排索引来查, Inverted就表示倒排查询).
    // term 表示所分的词, 因为用户输入的肯定不止一个字符, 因此所传参数应该是分好的词.
    // 返回结果应该是 Integer, 因为倒排查询结果是一个文档id的结果集.
    // 但是词和文档之间是具有一定的 "相关性" 的, 查询出的结果应该是相关性强的在前面, 弱的在后面.
    public List<Weight> getInverted(String term) {
        return invertedIndex.get(term);
    }

    // 3. 得将 Parser 类解析出来的信息添加到索引中, 即 往索引中新增一个文档.
    public void addDoc(String title, String url, String content) {
        // 新增文档需要同时给 正排索引 和 倒排索引 中新增文档信息
        // 1. 构建正排索引
        DocInfo docInfo = buildForward(title, url, content);

        // 2. 构建倒排索引
        buildInverted(docInfo);
    }

    private void buildInverted(DocInfo docInfo) {

        // 使用一个内部类来表示词在正文与标题中出现的次数
        class WordCnt {
            // 该变量表示这个词在 标题 中出现的次数
            public int titleCount;
            // 该变量表示这个词在 正文 中出现的次数
            public int contentCount;
        }

        // 构建一个 HashMap 来统计该词在文章中标题与正文里面出现的次数
        // @String 表示 词
        // @WordCnt 表示 出现在正文与标题中的次数
        HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();

        // 1. 针对文档标题进行分词.(因为传进来的DocInfo已经将标题和正文区分了, 所以这里分词就很方便)
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();

        //    遍历分词结果, 统计每个词出现的次数.
        for (Term term : terms) {
            // 先获取到词(注意: Term 里面包含词的很多属性, 词名称是需要用方法去获取的)
            String termWord = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(termWord);
            if (wordCnt == null) {
                // 如果 term 不存在, 则创建一个新的键值对, titleCount 设为1
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                newWordCnt.contentCount = 0;
                wordCntHashMap.put(termWord, newWordCnt);
            } else {
                // 如果 term 存在, 则把对应的 titleCount + 1
                wordCnt.titleCount += 1;
            }
        }
        // 2. 针对正文页进行分词.
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        //    遍历分词结果, 统计每个词出现的次数.
        for (Term term : terms) {
            // 和上面的流程差不多
            String termWord = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(termWord);
            if (wordCnt == null) {
                // 如果 term 不存在, 则创建一个新的键值对, contentCount 设为1
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 0;
                newWordCnt.contentCount = 1;
                wordCntHashMap.put(termWord, newWordCnt);
            } else {
                // 如果 term 存在, 则把对应的 contentCount + 1
                wordCnt.contentCount += 1;
            }
        }

        // 3. 遍历上面的 HashMap, 依次更新倒排索引结构.
        for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {
            // 先根据词去倒排索引中查找.
            // 拿到文章列表
            // 这里锁对象的选择去看笔记的详解
            synchronized (locker2) {
                List<Weight> invertedList = invertedIndex.get(entry.getKey());
                if (invertedList == null) {
                    // 如果文章列表为空, 就插入一个新的键值对
                    ArrayList<Weight> newInvertedList = new ArrayList<>();

                    // 把新的文档(即当前 DocInfo), 构造成 Weight 对象
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());

                    // 权重计算公式: 在标题出现的次数 * 30 + 在正文出现的次数
                    weight.setWeight(entry.getValue().titleCount * 30 + entry.getValue().contentCount);
                    newInvertedList.add(weight);

                    // 文章列表构造完毕, 放入倒排索引结构中
                    invertedIndex.put(entry.getKey(), newInvertedList);
                } else {
                    // 如果文章列表非空, 说明该词在倒排索引中存在.
                    // 虽然存在, 但是文章id是不同的, 因此将当前文档构造一个 Weigh 对象, 插入倒排索引中.
                    Weight weight = new Weight();

                    // 新增该词所在的文档id
                    weight.setDocId(docInfo.getDocId());

                    // 计算该词在该文章中的权重
                    weight.setWeight(entry.getValue().titleCount * 30 + entry.getValue().contentCount);

                    // 将该文章添加到对应 value 的数组链后面
                    invertedList.add(weight);
                }
            }

        }
    }

    private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        // 这里锁对象的选择去看笔记的详解
        synchronized (locker1) {
            // 因为是用数组下标表示文章id, 因此 DocId 就为插入前 forwardIndex 数组的长度.
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }

    // 4. 把内存中的索引结构保存到磁盘中.
    public void save() {
        //long begin = System.currentTimeMillis();
        //System.out.println("开始保存索引操作");
        // 使用两个文件来分别保存正排和倒排索引
        // 1. 可以先判定保存目录路径是否存在
        File savePathFile = new File(SAVE_PATH);
        if (!savePathFile.exists()) {
            // 如果该保存路径不存在, 则创建一个
            savePathFile.mkdirs(); //
        }
        // 保存正排索引文件
        File forwardIndexFile = new File(SAVE_PATH + "forward_index.txt");

        // 保存倒排索引文件
        File invertedIndexFile = new File(SAVE_PATH + "inverted_index.txt");

        // 第一个参数表示写入文件位置, 第二个参数表示写入的数据
        try {
            // 借助 Jackson库 里面的对象方法将索引结构 序列化
            objectMapper.writeValue(invertedIndexFile, invertedIndex);
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        //long end = System.currentTimeMillis();
        //System.out.println("结束保存索引操作, 代码消耗时间:" + (end - begin) + "ms");
    }

    // 5. 把磁盘中的索引数据加载到内存中.
    public void load() {
        //long begin = System.currentTimeMillis();
        //System.out.println("开始加载索引操作");
        // 1. 设置所加载文件的路径(与上面保存路径相同)
        File forwordIndexFile = new File(SAVE_PATH + "forward_index.txt");
        File invertedIndexFile = new File(SAVE_PATH + "inverted_index.txt");

        try {
            // 使用 Jackson库 里面的方法加载文件
            // 第一个参数是所加载文件的路径, 第二个参数表示将文件里面的数据用什么样的对象来接收(即反序列化的接收对象)
            // 接收的对象应该是我们所设置的 ArrayList<DocInfo>, Jackson专门提供了一个辅助的工具类帮助我们转换
            forwardIndex = objectMapper.readValue(forwordIndexFile, new TypeReference<ArrayList<DocInfo>>() {});
            invertedIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
        } catch (IOException e) {
            e.printStackTrace();
        }
        //long end = System.currentTimeMillis();
        //System.out.println("结束加载索引操作, 代码消耗时间:" + (end - begin) + "ms");
    }

    public static void main(String[] args) {
        Index index = new Index();
        index.load();
        System.out.println("加载索引结束");
    }
}
